<template>
  <div class="fx-particle" v-bind="$attrs" @mouseenter="mouseEnterInner=true" @mouseleave="mouseEnterInner=false"
       @mousemove="canvasMousemove" @click.stop="canvasAddClick">
    <canvas ref="canvasDom"></canvas>
  </div>
</template>

<script lang="ts">
import { defineComponent, nextTick, onBeforeMount, onMounted, PropType, reactive, ref, SetupContext, watch } from 'vue'

interface Dots {
  x: number;
  y: number;
  r: number;
  color: string;
  lineColor: string;
  speedX: number;
  speedY: number;
  pathX: number;
  pathY: number;
}

interface ParticleProps {
  dots: number;
  radius: number;
  randomRadius: boolean;
  speedX: number;
  speedY: number;
  connect: number;
  color: string | 'random';
  lineColor: string | 'random';
  mouseLine: number;
  mouseLineColor: string;
  isAdd: boolean;
  max: number;
  stop: boolean;
}

export default defineComponent({
  name: 'fxParticle',
  inheritAttrs: true,
  props: {
    // 运动点数
    dots: {
      type: Number as PropType<number>,
      default: 100
    },
    // 粒子点半径
    radius: {
      type: Number as PropType<number>,
      default: 2
    },
    // 粒子点半径
    randomRadius: {
      type: Boolean as PropType<boolean>,
      default: false
    },
    // 粒子点横向速度
    speedX: {
      type: Number as PropType<number>,
      default: 2
    },
    // 粒子点纵向速度
    speedY: {
      type: Number as PropType<number>,
      default: 2
    },
    // 粒子点颜色
    color: {
      type: String as PropType<string | 'random'>,
      default: '#ccc'
    },
    // 连接线颜色
    lineColor: {
      type: String as PropType<string | 'random'>,
      default: '#ccc'
    },
    // 粒子点间间距
    connect: {
      type: Number as PropType<number>,
      default: 100
    },
    // 鼠标移入连线长度
    mouseLine: {
      type: Number as PropType<number>,
      default: 0
    },
    // 鼠标移入连线颜色
    mouseLineColor: {
      type: String as PropType<string>,
      default: '#f60'
    },
    // 是否动态添加
    isAdd: {
      type: Boolean as PropType<boolean>,
      default: false
    },
    // 最大粒子个数
    max: {
      type: Number as PropType<number>,
      default: 150
    },
    // 暂停
    stop: {
      type: Boolean as PropType<boolean>,
      default: false
    }
  },
  emits: ['dotsChange'],
  setup (props: ParticleProps, setupCtx: SetupContext) {
    // 动画函数
    let particleAnimationFrame: null | number = null
    // 窗口变化 等待刷新定时器
    let particleTimer: ReturnType<typeof setTimeout> | null = null
    // 刷新时 暂停动画
    let refreshStop = false
    // 画布dom
    const canvasDom = ref<HTMLCanvasElement>()
    // 画布context
    const context = ref<CanvasRenderingContext2D>()
    // 鼠标是否移入
    const mouseEnterInner = ref<boolean>(false)
    // 粒子点数据数组
    let dotList = reactive<Dots[]>([])
    // 鼠标移入数据
    let mouseInfo = reactive<{ x: number, y: number }>({
      x: 0,
      y: 0
    })
    // 监听暂停属性 当关闭暂停时 粒子点动画开启
    watch(() => props.stop, (val: boolean): void => {
      !val ? playing() : particleAnimationFrame && cancelAnimationFrame(particleAnimationFrame)
    })
    // 监听暂停属性 当关闭暂停时 粒子点动画开启
    watch(() => props.speedX, (val: number): void => {
      dotList.forEach((item: Dots) => {
        item.speedX = Math.max(Math.random() * val, val / 4)
      })
    })
    // 监听暂停属性 当关闭暂停时 粒子点动画开启
    watch(() => props.speedY, (val: number): void => {
      dotList.forEach((item: Dots) => {
        item.speedY = Math.max(Math.random() * val, val / 4)
      })
    })
    // 监听暂停属性 当关闭暂停时 粒子点动画开启
    watch(() => props.color, (val: string): void => {
      dotList.forEach((item: Dots) => {
        item.color = randomColor(val)
      })
    })
    // 监听暂停属性 当关闭暂停时 粒子点动画开启
    watch(() => props.lineColor, (val: string): void => {
      dotList.forEach((item: Dots) => {
        item.lineColor = randomColor(val)
      })
    })

    // 清空画布
    const clearCanvas = () => {
      if (!canvasDom.value || !context.value) return
      const rect = canvasDom.value.getBoundingClientRect()
      context.value.clearRect(0, 0, rect.width, rect.height)
    }
    // 粒子点运动
    const playing = () => {
      if (refreshStop || props.stop) return
      clearCanvas()
      dotsPlaying()
      setupCtx.emit('dotsChange', dotList.length)
      particleAnimationFrame = requestAnimationFrame(playing)
    }
    // 初始化
    const initParticle = (): void => {
      if (!canvasDom?.value) return
      context.value = canvasDom.value.getContext('2d') as CanvasRenderingContext2D
      canvasDom.value.width = canvasDom.value?.clientWidth
      canvasDom.value.height = canvasDom.value?.clientHeight
      playing()
      initDots()
    }
    // 初始化粒子点数组
    const initDots = (): void => {
      const rect = canvasDom.value?.getBoundingClientRect()
      if (!rect) return
      dotList = []
      Array.from(new Array(Math.min(props.dots, props.max))).forEach(() => {
        const x = Math.floor(Math.random() * rect.width)
        const y = Math.floor(Math.random() * rect.height)
        const disX = x <= props.radius ? props.radius : x >= (rect.width - props.radius) ? (rect.width - props.radius) : x
        const disY = y <= props.radius ? props.radius : y >= (rect.height - props.radius) ? (rect.height - props.radius) : y
        addDots(disX, disY)
      })
    }
    // 初始化粒子点数据
    const addDots = (x: number, y: number): void => {
      dotList.push({
        x,
        y,
        color: randomColor(props.color),
        lineColor: randomColor(props.lineColor),
        r: props.randomRadius ? Math.max(Math.random() * props.radius * 1.5, props.radius * 0.5) : props.radius,
        speedX: Math.max(Math.random() * props.speedX, props.speedX / 4),
        speedY: Math.max(Math.random() * props.speedY, props.speedY / 4),
        pathX: Math.random() > 0.5 ? 1 : -1,
        pathY: Math.random() > 0.5 ? 1 : -1
      })
    }
    // 勾股定理计算是否连线
    const isConnect = (x1: number, y1: number, x2: number, y2: number): number => {
      return Math.sqrt(Math.abs(x1 - x2) ** 2 + Math.abs((y1 - y2) ** 2))
    }
    // 绘制粒子点数组
    const dotsPlaying = () => {
      if (!canvasDom.value) return
      const rect = canvasDom.value.getBoundingClientRect()
      dotList.forEach((item: Dots, itemIndex: number) => {
        if (item.x - item.r <= 0) {
          item.x = item.r
          item.pathX = -item.pathX
        }
        if (item.x + item.r >= rect.width) {
          item.x = rect.width - item.r
          item.pathX = -item.pathX
        }
        if (item.y - item.r <= 0) {
          item.y = item.r
          item.pathY = -item.pathY
        }
        if (item.y + item.r >= rect.height) {
          item.y = rect.height - item.r
          item.pathY = -item.pathY
        }
        item.x += item.speedX * item.pathX
        item.y += item.speedY * item.pathY
        if (mouseEnterInner.value && props.mouseLine > 0 && isConnect(item.x, item.y, mouseInfo.x, mouseInfo.y) < props.mouseLine) {
          createLine(item.x, item.y, mouseInfo.x, mouseInfo.y, props.mouseLineColor)
        }
        if (props.connect > 0) {
          dotList.forEach((child: Dots, childIndex: number) => {
            if (itemIndex === childIndex) return
            const connectLine = isConnect(item.x, item.y, child.x, child.y)
            if (connectLine <= props.connect) {
              createLine(item.x, item.y, child.x, child.y, item.lineColor)
            }
          })
        }
        createArc(item.x, item.y, item.r, item.color)
      })
    }
    // 绘制线
    const createLine = (x1: number, y1: number, x2: number, y2: number, color: string): void => {
      const ctx = context.value as CanvasRenderingContext2D
      ctx.beginPath()
      ctx.moveTo(x1, y1)
      ctx.lineTo(x2, y2)
      ctx.lineWidth = 0
      ctx.strokeStyle = color
      ctx.stroke()
      ctx.closePath()
    }
    // 绘制点
    const createArc = (x: number, y: number, r: number, color: string): void => {
      const ctx = context.value as CanvasRenderingContext2D
      ctx.beginPath()
      ctx.arc(x, y, r, 0, 2 * Math.PI)
      ctx.fillStyle = color
      ctx.fill()
      ctx.closePath()
    }
    // 鼠标移入
    const canvasMousemove = (ev: MouseEvent): void => {
      if (props.mouseLine <= 0) return
      const rect = canvasDom.value?.getBoundingClientRect()
      if (rect) {
        mouseEnterInner.value = true
        mouseInfo = {
          x: ev.x - rect.x,
          y: ev.y - rect.y
        }
      }
    }
    // 动态添加粒子点
    const canvasAddClick = (ev: MouseEvent): void => {
      if (!props.isAdd) return
      const rect = canvasDom.value?.getBoundingClientRect()
      if (rect) {
        // 当动态添加超出最大上限时 从数组开始处删除多余粒子
        if (dotList.length > props.max) dotList.splice(0, 1)
        addDots(ev.x - rect.x, ev.y - rect.y)
      }
    }
    // 重新刷新
    const refresh = () => {
      refreshStop = true
      particleAnimationFrame && cancelAnimationFrame(particleAnimationFrame)
      clearCanvas()
      particleTimer && clearTimeout(particleTimer)
      particleTimer = setTimeout(() => {
        refreshStop = false
        initParticle()
      }, 200)
    }
    // 随机色
    const randomColor = (val: string): string => {
      return val === 'random' ? `rgb(${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)},${Math.floor(Math.random() * 256)})` : val
    }
    onMounted(() => {
      nextTick(() => {
        initParticle()
        window.addEventListener('resize', refresh)
      })
    })
    onBeforeMount(() => {
      window.removeEventListener('resize', refresh)
    })

    return {
      canvasDom,
      mouseEnterInner,
      refresh,
      canvasAddClick,
      canvasMousemove
    }
  }
})
</script>
